/*
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <stdarg.h>
#include <stdbool.h>
#include <fcntl.h>

#include <glib.h>
#include <glib-object.h>
#include <gst/gst.h>

#include "../libgalaxium.h"
#include "../devices.h"
#include "videosink.h"

void destroy_pipeline (videoInfo *info);
static gboolean bus_callback (GstBus *bus, GstMessage *msg, gpointer user_data);
static void cb_receivesrc_handoff (GstElement *src, GstBuffer *buffer, GstPad *pad, gpointer user_data);

void gstreamer_video_new_frame (videoInfo *info);
void gstreamer_video_new_size (videoInfo *info, gint width, gint height);

#define CHECK_ELEMENT(elem) if (elem == NULL) { g_print ("ERROR: Unable to create " #elem "\n"); g_free (info); return NULL; }

GalaxiumWebcamDevice *gstreamer_video_get_devices (int *num)
{
	return libgalaxium_find_webcams (num);
}

videoInfo *gstreamer_video_new (GalaxiumWebcamDevice *device, GalaxiumVideoFormat *format, GalaxiumFramerate *framerate)
{
	GstElement *pipeline, *src, *csp, *scale, *scaleflt, *sink;
	GstBus *bus;
	
	videoInfo* info = g_new0 (videoInfo, 1);
	
	// Pipeline with device != NULL:
	// 
	// v4lsrc -> tee -> ffmpegcolorspace -> capsfilter -> videoscale -> scale capsfilter -> sink (-> cairo)
	//              |
	//              |-> encoder -> fakesink (-> galaxium)
	//              |-> pngenc -> fakesink (-> galaxium)
	
	// Pipeline with device == NULL:
	// 
	// (galaxium ->) fakesrc -> decoder -> ffmpegcolorspace -> videoscale -> scale capsfilter -> sink (-> cairo)
	
	pipeline = gst_pipeline_new ("pipeline");
	csp = gst_element_factory_make ("ffmpegcolorspace", "csp");
	scale = gst_element_factory_make ("videoscale", "scale");
	scaleflt = gst_element_factory_make ("capsfilter", "scaleflt");
	sink = galaxium_video_sink_new ((NewSizeMethod)gstreamer_video_new_size, info,
									(NewFrameMethod)gstreamer_video_new_frame, info);
	
	CHECK_ELEMENT (pipeline)
	CHECK_ELEMENT (csp)
	CHECK_ELEMENT (scale)
	CHECK_ELEMENT (scaleflt)
	CHECK_ELEMENT (sink)
	
	gst_bin_add_many (GST_BIN (pipeline), csp, scale, scaleflt, sink, NULL);
	
	info->pipeline = pipeline;
	info->scaleflt_pre = scale;
	info->scaleflt = scaleflt;
	info->scaleflt_post = sink;
	info->sink = (GalaxiumVideoSink *)sink;
	
	if (device != NULL)
	{
		GstElement *tee;
		GstElement *flt;
		GstElement *encoder;
		GstElement *sendsink;
		
		src = gst_element_factory_make (device->gstreamer_src, "source");
		tee = gst_element_factory_make ("tee", "tee");
		flt = gst_element_factory_make ("capsfilter", "flt");
		encoder = gst_element_factory_make ("identity", "encoder");
		sendsink = gst_element_factory_make ("fakesink", "sendsink");
		
		CHECK_ELEMENT (src)
		CHECK_ELEMENT (tee)
		CHECK_ELEMENT (flt)
		CHECK_ELEMENT (encoder)
		CHECK_ELEMENT (sendsink)
		
		//TODO: uncommenting the tee seems to screw everything up...
		
		gst_bin_add_many (GST_BIN (pipeline), src, /*tee,*/ flt, /*encoder, sendsink,*/ NULL);
		gst_element_link_many (src, /*tee, NULL);
		gst_element_link_many (tee,*/ csp, flt, scale, scaleflt, sink, NULL);
		//gst_element_link_many (tee, encoder, sendsink, NULL);
		
		g_object_set (G_OBJECT (src), "device", device->video_device, NULL);
		
		g_object_set (G_OBJECT (flt), "caps",
			gst_caps_new_simple ("video/x-raw-rgb",
					     "width", G_TYPE_INT, format->width,
					     "height", G_TYPE_INT, format->height,
					     "framerate", GST_TYPE_FRACTION, framerate->numerator, framerate->denominator,
					     NULL), NULL);
		
		info->device = g_memdup (device, sizeof (GalaxiumWebcamDevice));
		info->format = g_memdup (format, sizeof (GalaxiumVideoFormat));
		info->framerate = g_memdup (framerate, sizeof (GalaxiumFramerate));
		
		info->tee = tee;
		info->encoder_pre = tee;
		info->encoder = encoder;
		info->encoder_post = sendsink;
		info->sendsink = sendsink;
	}
	else
	{
		GstElement *decoder;
		
		src = gst_element_factory_make ("fakesrc", "source");
		decoder = gst_element_factory_make ("identity", "decoder");
		
		CHECK_ELEMENT (src)
		CHECK_ELEMENT (decoder)
		
		gst_bin_add_many (GST_BIN (pipeline), src, decoder, NULL);
		gst_element_link_many (src, decoder, csp, scale, scaleflt, sink, NULL);
		
		g_object_set (G_OBJECT (src),
						"signal-handoffs", TRUE,
						"sizetype", 2 /*FAKE_SRC_SIZETYPE_FIXED*/, NULL);
		
		g_signal_connect (src, "handoff", G_CALLBACK (cb_receivesrc_handoff), NULL);
		
		info->receivesrc = src;
		info->decoder_pre = src;
		info->decoder = decoder;
		info->decoder_post = csp;
	}
	
	g_object_set (G_OBJECT (scale), "method", 1 /*GST_VIDEO_SCALE_BILINEAR*/, NULL);
	g_object_set (G_OBJECT (scaleflt), "caps", gst_caps_new_simple ("video/x-raw-rgb", NULL), NULL);
	
	bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
	gst_bus_add_watch (bus, bus_callback, info);
	gst_object_unref (bus);
	
	return info;
}

videoInfo *gstreamer_video_new_local (GalaxiumWebcamDevice *device, GalaxiumVideoFormat *format, GalaxiumFramerate *framerate)
{
	return gstreamer_video_new (device, format, framerate);
}

videoInfo *gstreamer_video_new_remote ()
{
	return gstreamer_video_new (NULL, NULL, NULL);
}

void gstreamer_video_free (videoInfo *info)
{
	destroy_pipeline (info);
	
	if (info->device != NULL)
	{
		g_free (info->device);
		g_free (info->format);
		g_free (info->framerate);
	}
	
	if (info->receivesrc_buffer != NULL)
		g_free (info->receivesrc_buffer);
	
	g_free (info);
}

void gstreamer_video_play (videoInfo *info)
{
	info->playing = TRUE;
	gst_element_set_state (info->pipeline, GST_STATE_PLAYING);
}

void gstreamer_video_stop (videoInfo* info)
{
	gst_element_set_state (info->pipeline, GST_STATE_NULL);
	info->playing = FALSE;
}

void gstreamer_video_write (videoInfo *info, const gchar *buf, gint count)
{
	if (info->receivesrc_size != count)
	{
		g_print ("libgalaxium: New buffer size %d\n", count);
		
		g_object_set (G_OBJECT (info->receivesrc), "sizemax", count, NULL);
		info->receivesrc_size = count;
		
		if (info->receivesrc_buffer != NULL)
			g_free (info->receivesrc_buffer);
	}
	
	if (info->receivesrc_buffer == NULL)
		info->receivesrc_buffer = g_memdup (buf, count);
	else
		g_memmove (info->receivesrc_buffer, buf, count);
}

void gstreamer_video_set_decoder (videoInfo *info, gchar *decstr)
{
	gboolean playing = info->playing;
	
	gstreamer_video_stop (info);
	
	g_print ("Switch decoder to %s\n", decstr);
	
	gst_element_unlink_many (info->decoder_pre, info->decoder, info->decoder_post, NULL);
	gst_bin_remove (GST_BIN (info->pipeline), info->decoder);
	
	info->decoder = gst_element_factory_make (decstr, "decoder");
	
	if (info->decoder == NULL)
	{
		g_print ("Decoder is NULL!\n");
		info->decoder = gst_element_factory_make ("identity", "decoder");
	}
	
	gst_bin_add (GST_BIN (info->pipeline), info->decoder);
	gst_element_link_many (info->decoder_pre, info->decoder, info->decoder_post, NULL);
	
	g_print ("Decoder switch complete\n");
	
	if (playing == TRUE)
		gstreamer_video_play (info);
}

void gstreamer_video_set_encoder (videoInfo *info, gchar *encstr)
{
	gboolean playing = info->playing;
	
	gstreamer_video_stop (info);
	
	gst_element_unlink_many (info->encoder_pre, info->encoder, info->encoder_post, NULL);
	gst_bin_remove (GST_BIN (info->pipeline), info->encoder);
	
	info->encoder = gst_element_factory_make (encstr, "encoder");
	
	gst_bin_add (GST_BIN (info->pipeline), info->encoder);
	gst_element_link_many (info->encoder_pre, info->encoder, info->encoder_post, NULL);
	
	if (playing == TRUE)
		gstreamer_video_play (info);
}

void gstreamer_video_set_surface (videoInfo *info, cairo_surface_t *surface)
{
	galaxium_video_sink_set_surface (info->sink, surface);
}

void gstreamer_video_set_scale (videoInfo *info, gint width, gint height)
{
	gboolean playing = info->playing;
	
	gstreamer_video_stop (info);
	
	//gst_element_unlink_many (info->scaleflt_pre, info->scaleflt, info->scaleflt_post, NULL);
	//gst_bin_remove (GST_BIN (info->pipeline), info->scaleflt);
	
	//info->scaleflt = gst_element_factory_make ("capsfilter", "scaleflt");
	
	g_object_set (G_OBJECT (info->scaleflt), "caps",
  		gst_caps_new_simple ("video/x-raw-rgb",
				     "width", G_TYPE_INT, width,
				     "height", G_TYPE_INT, height,
				     NULL), NULL);
	
	//gst_bin_add (GST_BIN (info->pipeline), info->scaleflt);
	//gst_element_link_many (info->scaleflt_pre, info->scaleflt, info->scaleflt_post, NULL);
	
	if (playing == TRUE)
		gstreamer_video_play (info);
}

void gstreamer_video_set_newframe_callback (videoInfo *info, ManagedNewFrameCallback cb)
{
	info->newFrameCallback = cb;
}

void gstreamer_video_set_newsize_callback (videoInfo *info, ManagedNewSizeCallback cb)
{
	info->newSizeCallback = cb;
}

void gstreamer_video_set_error_callback (videoInfo *info, ManagedErrorCallback cb)
{
	info->errorCallback = cb;
}

void destroy_pipeline (videoInfo *info)
{
	if (info->pipeline != NULL)
	{
		gst_element_set_state (GST_ELEMENT (info->pipeline), GST_STATE_NULL);
		gst_object_unref (GST_OBJECT (info->pipeline));
		info->pipeline = NULL;
	}
}

void gstreamer_video_new_size (videoInfo *info, gint width, gint height)
{
	if (info->newSizeCallback != NULL)
		info->newSizeCallback (info, width, height);
}

void gstreamer_video_new_frame (videoInfo *info)
{
	if (info->newFrameCallback != NULL)
		info->newFrameCallback (info);
}

gboolean bus_callback (GstBus *bus, GstMessage *msg, gpointer user_data)
{
	videoInfo *info = (videoInfo *)user_data;

	if (info == NULL)
		return FALSE;
	
	switch (GST_MESSAGE_TYPE (msg))
	{
		case GST_MESSAGE_ERROR:
		{
			gchar *debug;
			GError *err;

			destroy_pipeline (info);
			
			gst_message_parse_error (msg, &err, &debug);
			
			if (info->errorCallback != NULL)
				info->errorCallback (info, err->domain, err->code, err->message, debug);
			
			g_free (debug);
			g_error_free (err);
	
			break;
		}
		
		default:
			break;
	}
	
	return TRUE;
}

void cb_receivesrc_handoff (GstElement *src, GstBuffer *buffer, GstPad *pad, gpointer user_data)
{
	videoInfo *info = (videoInfo *)user_data;
	
	if (info == NULL)
		return;
	
	if (info->receivesrc_buffer != NULL)
	{
		g_memmove (GST_BUFFER_DATA (buffer), info->receivesrc_buffer, GST_BUFFER_SIZE (buffer));
	}
	else
	{
		// Solid black
		memset (GST_BUFFER_DATA (buffer), 0x0, GST_BUFFER_SIZE (buffer));
	}
}
